package com.zee.utils;

import com.zee.annotation.Interceptor;
import com.zee.annotation.ActivityTag;

import java.io.BufferedWriter;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;

import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;

import de.greenrobot.common.ListMap;

public class RouterInfoMakeUtil {
    private final ListMap<TypeElement, ExecutableElement> methodsByClass = new ListMap<>();
    private final Set<TypeElement> classesToSkip = new HashSet<>();

    private Elements elements;
    private Types types;
    ProcessingEnvironment processingEnv;
    public static final String ACTIVITY = "android.app.Activity";
    public static final String FRAGMENT = "android.app.Fragment";
    public static final String FRAGMENT_V4 = "android.support.v4.app.Fragment";
    Logger logger;

    public RouterInfoMakeUtil(ProcessingEnvironment processingEnv, Logger logger) {
        this.processingEnv = processingEnv;
        this.logger = logger;
        elements = processingEnv.getElementUtils();      // Get class meta.
        types = processingEnv.getTypeUtils();
    }


    public void createInfoIndexFile(String index) {
        BufferedWriter writer = null;
        try {
            JavaFileObject sourceFile = processingEnv.getFiler().createSourceFile(index);
            int period = index.lastIndexOf('.');
            String myPackage = period > 0 ? index.substring(0, period) : null;
            String clazz = index.substring(period + 1);
            writer = new BufferedWriter(sourceFile.openWriter());
            if (myPackage != null) {
                writer.write("package " + myPackage + ";\n\n");
            }
            writer.write("import com.zee.bean.RouteBean;\n");
            writer.write("import com.zee.bean.InterceptorBean;\n");
            writer.write("import com.zee.interf.IRoute;\n\n");
            writer.write("import android.text.TextUtils;\n\n");
            writer.write("import com.zee.log.ZLog;\n\n");

            writer.write("import java.util.HashMap;\n");
            writer.write("import java.util.ArrayList;\n");
            writer.write("import java.util.Map;\n\n");
            writer.write("/** This class is generated by EventBus, do not edit. */\n");
            writer.write("public class " + clazz + " implements IRoute {\n");
            writer.write("    private static final Map<String, RouteBean> SUBSCRIBER_INDEX=new HashMap<String, RouteBean>();\n");
            writer.write("    private static final Map<Class<?>, RouteBean> ROTERCLSSS_INDEX = new HashMap<Class<?>, RouteBean>();\n");
            writer.write("    private static final ArrayList<InterceptorBean> INTERCEPTORS = new ArrayList<>();\n\n");
            writer.write("    static {\n");
            writeIndexLines(writer, myPackage);
            writer.write("    }\n\n");

            writer.write("    private static void addRouteBean(RouteBean bean) {\n");
            writer.write("        StringBuilder builder = new StringBuilder(bean.getName());\n");

            writer.write("        String moduleName = bean.getModule();\n");
            writer.write("        if (!TextUtils.isEmpty(moduleName)) {\n");
            writer.write("          builder.append(\"$$\");\n");
            writer.write("          builder.append(bean.getModule());\n");
            writer.write("        }\n");

            writer.write("        String key = builder.toString();\n");
            writer.write("        boolean isHave = SUBSCRIBER_INDEX.containsKey(key);\n");
            writer.write("        if (isHave) {\n");
            writer.write("              ZLog.e(\"key is Same:\" + SUBSCRIBER_INDEX.get(key) + \"and\\n\" + bean);\n");
            writer.write("        }\n");

            writer.write("        SUBSCRIBER_INDEX.put(key, bean);\n");
            writer.write("        ROTERCLSSS_INDEX.put(bean.getRouteClass(), bean);\n");
            writer.write("    }\n\n");


            writer.write("    private static void addInterceptor(InterceptorBean bean) {\n");
            writer.write("        INTERCEPTORS.add(bean);\n");
            writer.write("    }\n\n");


            writer.write("    @Override\n");
            writer.write("    public RouteBean getRouteBean(String name) {\n");
            writer.write("        return SUBSCRIBER_INDEX.get(name);\n");
            writer.write("    }\n");


            writer.write("    @Override\n");
            writer.write("    public RouteBean getRouteBean(Class<?> classz) {\n");
            writer.write("        return ROTERCLSSS_INDEX.get(classz);\n");
            writer.write("    }\n");


            writer.write("    @Override\n");
            writer.write("    public ArrayList<InterceptorBean> getInterceptors() {\n");
            writer.write("        return INTERCEPTORS;\n");
            writer.write("    }\n");

            writer.write("}\n");
        } catch (IOException e) {
            throw new RuntimeException("Could not write source for " + index, e);
        } finally {
            if (writer != null) {
                try {
                    writer.close();
                } catch (IOException e) {
                    //Silent
                }
            }
        }
    }

    /**
     * Subscriber classes should be skipped if their class or any involved event class are not visible to the index.
     */
    public void checkForSubscribersToSkip(Messager messager, String myPackage) {
        for (TypeElement skipCandidate : methodsByClass.keySet()) {
            TypeElement subscriberClass = skipCandidate;
            while (subscriberClass != null) {
                if (!isVisible(myPackage, subscriberClass)) {
                    boolean added = classesToSkip.add(skipCandidate);
                    if (added) {
                        String msg;
                        if (subscriberClass.equals(skipCandidate)) {
                            msg = "Falling back to reflection because class is not public";
                        } else {
                            msg = "Falling back to reflection because " + skipCandidate +
                                    " has a non-public super class";
                        }
                        messager.printMessage(Diagnostic.Kind.NOTE, msg, subscriberClass);
                    }
                    break;
                }
                subscriberClass = getSuperclass(subscriberClass);
            }
        }
    }


    private void writeIndexLines(BufferedWriter writer, String myPackage) throws IOException {
        TypeMirror type_Activity = elements.getTypeElement(ACTIVITY).asType();
        TypeMirror type_Fragment = elements.getTypeElement(FRAGMENT_V4).asType();

        for (TypeElement subscriberTypeElement : methodsByClass.keySet()) {
            if (classesToSkip.contains(subscriberTypeElement)) {
                continue;
            }
            String subscriberClass = getClassString(subscriberTypeElement, myPackage);
            if (isVisible(myPackage, subscriberTypeElement)) {
                ActivityTag tempActivityTag = subscriberTypeElement.getAnnotation(ActivityTag.class);
                TypeMirror tm = subscriberTypeElement.asType();


                Interceptor tempInterceptor = subscriberTypeElement.getAnnotation(Interceptor.class);
                if (tempInterceptor != null) {
                    writeLine(writer, 2,
                            "addInterceptor( new InterceptorBean(" + subscriberClass + ".class,",
                            "\"" + tempInterceptor.name() + "\",", "\"" + tempInterceptor.module() + "\"");
                    String infor = "";
                    if (tempInterceptor != null) {
                        infor = tempInterceptor.keyWord();
                    }
                    writer.write(",\"" + infor + "\"," + tempInterceptor.priority() + "));\n");

                } else if (tempActivityTag != null) {
                    String typeInfo = "0";

                    if (types.isSubtype(tm, type_Activity)) {
                        typeInfo = "1";
                    } else if (types.isSubtype(tm, type_Fragment)) {
                        typeInfo = "2";
                    } else {
                        logger.error(subscriberClass + " is not type: Activity or v4.app.Fragment");
                    }

                    writeLine(writer, 2,
                            "addRouteBean(new RouteBean(" + typeInfo + "," + subscriberClass + ".class,",
                            "\"" + tempActivityTag.name() + "\",", "\"" + tempActivityTag.module() + "\"");
                    String infor = "";
                    if (tempActivityTag != null) {
                        infor = tempActivityTag.keyWords();
                    }
                    writer.write(",\"" + infor + "\"));\n");
                }
            } else {
                writer.write("        // Subscriber not visible to index: " + subscriberClass + "\n");
            }
        }
    }


    private TypeElement getSuperclass(TypeElement type) {
        if (type.getSuperclass().getKind() == TypeKind.DECLARED) {
            TypeElement superclass = (TypeElement) processingEnv.getTypeUtils().asElement(type.getSuperclass());
            String name = superclass.getQualifiedName().toString();
            if (name.startsWith("java.") || name.startsWith("javax.") || name.startsWith("android.")) {
                // Skip system classes, this just degrades performance
                return null;
            } else {
                return superclass;
            }
        } else {
            return null;
        }
    }

    private String getClassString(TypeElement typeElement, String myPackage) {
        PackageElement packageElement = getPackageElement(typeElement);
        String packageString = packageElement.getQualifiedName().toString();
        String className = typeElement.getQualifiedName().toString();
        if (packageString != null && !packageString.isEmpty()) {
            if (packageString.equals(myPackage)) {
                className = cutPackage(myPackage, className);
            } else if (packageString.equals("java.lang")) {
                className = typeElement.getSimpleName().toString();
            }
        }
        return className;
    }

    private String cutPackage(String paket, String className) {
        if (className.startsWith(paket + '.')) {
            // Don't use TypeElement.getSimpleName, it doesn't work for us with inner classes
            return className.substring(paket.length() + 1);
        } else {
            // Paranoia
            throw new IllegalStateException("Mismatching " + paket + " vs. " + className);
        }
    }


    private void writeLine(BufferedWriter writer, int indentLevel, String... parts) throws IOException {
        writeLine(writer, indentLevel, 2, parts);
    }

    private void writeLine(BufferedWriter writer, int indentLevel, int indentLevelIncrease, String... parts)
            throws IOException {
        writeIndent(writer, indentLevel);
        int len = indentLevel * 4;
        for (int i = 0; i < parts.length; i++) {
            String part = parts[i];
            if (i != 0) {
                if (len + part.length() > 118) {
                    writer.write("\n");
                    if (indentLevel < 12) {
                        indentLevel += indentLevelIncrease;
                    }
                    writeIndent(writer, indentLevel);
                    len = indentLevel * 4;
                } else {
                    writer.write(" ");
                }
            }
            writer.write(part);
            len += part.length();
        }
        writer.write("\n");
    }

    private void writeIndent(BufferedWriter writer, int indentLevel) throws IOException {
        for (int i = 0; i < indentLevel; i++) {
            writer.write("    ");
        }
    }

    private boolean isVisible(String myPackage, TypeElement typeElement) {
        Set<Modifier> modifiers = typeElement.getModifiers();
        boolean visible;
        if (modifiers.contains(Modifier.PUBLIC)) {
            visible = true;
        } else if (modifiers.contains(Modifier.PRIVATE) || modifiers.contains(Modifier.PROTECTED)) {
            visible = false;
        } else {
            String subscriberPackage = getPackageElement(typeElement).getQualifiedName().toString();
            if (myPackage == null) {
                visible = subscriberPackage.length() == 0;
            } else {
                visible = myPackage.equals(subscriberPackage);
            }
        }
        return visible;
    }


    private PackageElement getPackageElement(TypeElement subscriberClass) {
        Element candidate = subscriberClass.getEnclosingElement();
        while (!(candidate instanceof PackageElement)) {
            candidate = candidate.getEnclosingElement();
        }
        return (PackageElement) candidate;
    }

    public boolean isEmpty() {
        return methodsByClass.isEmpty();
    }


    public void collectSubscribers(Set<? extends TypeElement> annotations, RoundEnvironment env, Messager messager) {
        for (TypeElement annotation : annotations) {
            Set<? extends Element> elements = env.getElementsAnnotatedWith(annotation);
            for (Element element : elements) {
                if (element instanceof ExecutableElement) {
                    ExecutableElement method = (ExecutableElement) element;
//                    if (checkHasNoErrors(method, messager)) {
                    TypeElement classElement = (TypeElement) method.getEnclosingElement();
//                    }
                } else {
                    methodsByClass.putElement((TypeElement) element, null);
//                    messager.printMessage(Diagnostic.Kind.ERROR, "@Subscribe is only valid for methods", element);
                }
            }
        }
    }

}
